NULL equ 0x00
SETCHAR equ 0x07
VIDEOMEM equ 0xb800
STRINGLEN equ 0xffff

section head align=16 vstart=0
    Size dd ProgramEnd                      ;4B 0x00，整个程序的长度，包含头部分
    SegmentAddr:
    CodeSeg dd section.code.start           ;4B 0x04, nasm 提供的 '.' 语法，表示 code 段地址
    DataSeg dd section.data.start           ;4B 0x08
    StackSeg dd section.stack.start         ;4B 0x0C
    SegmentNum:
    SegNum db (SegmentNum-SegmentAddr)/4    ;1B 0x10，段的数量，由于每个段的地址固定占 4B，所以除以 4 就能知道具体有多少个段了
    Entry dw CodeStart                      ;2B 0x11，偏移地址
          dd section.code.start             ;4B 0x13，段地址

section code align=16 vstart=0
;声明程序入口点
CodeStart:
    mov ax, [DataSeg]   ;初始化 ds 和 si，指向字符串的开头
    mov ds, ax
    mov ax, [StackSeg]
    mov ss, ax
    mov sp, StackEnd
    xor si, si
    ;call ClearScreen
    call PrintString    ;调用打印方法
    jmp $

;声明 PrintString 函数
;用 [ds:si] 指向要显示的字符串的地址
;用 [es:di] 指向显存中的地址
PrintString:
    .setup:             ;设置 setup 标号，紧接着的代码是做一些初始化的工作
    push ax
    push bx
    push cx
    push dx
    mov ax, VIDEOMEM
    mov es, ax          ;将 ds 设置为显示字符的起始位置，用 si 作为偏移地址
    xor di, di

    mov bh, SETCHAR     ;显示字符的属性都为白底黑字
    mov cx, STRINGLEN   ;字符串长度最长为 0xffff，所以最多循环那么多次

    .printchar:         ;设置 printchar 标号，表示紧接着的代码是打印字符
    mov bl, [ds:si]     ;将要显示的字符存到 bl 中
    or bl, NULL         ;可以通过标志寄存器判断当前是不是读到了字符串的结尾
    jz .return          ;如果已经读到了字符串的结尾，则跳转到退出的标号处
    cmp bl, 0x0d        ;判断当前字符是否是回车
    jz .putCR           ;如果是回车，则跳转到输出回车的代码
    cmp bl, 0x0a        ;判断当前字符是否是换行
    jz .putLF           ;如果是换行，则跳转到输出换行的代码
    inc si              ;将 si + 1，指向下一个待显示的字符
    mov [es:di], bl     ;将 bl 中存储的字符设置到显存指定地址中
    inc di              ;为了防止覆盖，需要将显存地址 + 1
    mov [es:di], bh     ;设置该字符的属性，即白底黑字
    inc di
    call SetCursor
    jmp .loopEnd

    ;输出回车
    .putCR:
    mov bl, 160         ;一行 80 个字符，每个字符占用两个字节，一个字节表示字符，一个字节表示字符的属性，所以一共 160 个字节
    mov ax, di          ;保存当前 di 的大小
    div bl              ;商表示光标应该所在的行，余数表示光标应该所在的列，由于除数是 8 位，所以除法的结果 AL 保存商，AH 保存余数
    shr ax, 8           ;逻辑右移，将低位复制到 CF 标识位，高位用 0 填充，右移 8 位得到光标所在列
    sub di, ax          ;将光标所在的位置减去光标的列，就得到了光标所在行的行首的地址
    call SetCursor      ;调用设置光标的方法
    inc si              ;将 si + 1，指向下一个待显示的字符
    jmp .loopEnd        ;结束本次循环

    ;输出换行符
    .putLF:
    add di, 160         ;由于一行是 160 个字节，所以加上 160 光标自然就移动到了下一行的该位置
    call SetCursor      ;调用设置光标的方法
    inc si              ;将 si + 1，指向下一个待显示的字符
    jmp .loopEnd        ;结束本次循环

    .loopEnd:
    loop .printchar     ;继续打印下一个字符

    .return:
    mov bx, di
    pop dx
    pop cx
    pop bx
    pop ax
    ret

;设置光标的函数
;输入参数：di 光标所在的位置，从 0 开始到 (80 * 25 - 1)
;
SetCursor:
    ;在操作前先将要使用的寄存器压栈
    push dx         
    push bx
    push ax

    ;计算出光标的位置，由于 di 存储是字节数，而字节数又是字符数的 2 倍，所以除以 2 就是字符数
    mov ax, di      ;存储被除数的低 16 位
    mov dx, 0       ;存储被除数的高 16 位
    mov bx, 2       ;存储除数
    div bx          ;进行 16 位的除法，除法的结果 AX 保存商，DX 保存余数

    ;设置光标的位置
    ;当 0x3d4 端口为 0x0e 时，0x3d5 端口保存的是光标地址的高 8 位
    ;当 0x3d4 端口为 0x0f 时，0x3d5 端口保存的是光标地址的低 8 位
    mov bx, ax      ;暂存光标的位置
    mov dx, 0x3d4   ;存储控制端口
    mov al, 0x04    ;存储控制指令
    out dx, al      ;向控制端口发送控制指令，表示 0x3d5 端口将要写入的是光标地址的高 8 位
    mov dx, 0x3d5   ;存储数据端口
    mov al, bh      ;存储光标地址的高 8 位
    out dx, al      ;将光标地址的高 8 位，写入到数据端口

    mov dx, 0x3d4    ;存储控制端口
    mov al, 0x0f    ;存储控制指令
    out dx, al      ;向控制端口发送控制指令，表示 0x3d5 端口将要写入的是光标地址的低 8 位
    mov dx, 0x3d5   ;存储数据端口
    mov al, bl      ;存储光标地址的低 8 位
    out dx, al      ;将光标地址的低 8 位，写入到数据端口

    pop ax
    pop bx
    pop dx
    ret

section data align=16 vstart=0
    Hello db 'Hello,I com from program on sector 1,loaded by bootloader!'
          db 0x0d, 0x0a
          db 'Haha, This is a new line!'
          db 0x0a
          db 'Just 0a'
          db 0x0d
          db 'Just od'
          db 0x0d, 0x0a
          db 0x00
section stack align=16 vstart=0
    times 128 db 0
    StackEnd:
section end align=16
    ProgramEnd: ;这个标号主要作用就是计算整个程序的长度